package org.fjsei.yewu.security;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import md.system.UserRepository;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.security.Key;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.UUID;
import java.util.stream.Collectors;

@Slf4j
@Component
public class JwtTokenProvider implements InitializingBean {

    private static final String AUTHORITIES_KEY = "auth";
    private static final String DEVICE_ID_KEY = "deviceId";
    //密钥还不能太短了！报错not secure enough for any JWT HMAC-SHA algorithm. >= 256 bits
    private final String secret;
    private final long accessTokenValidityInMilliseconds;
    private final long refreshTokenValidityInMilliseconds;
    //转换后的密码
    private Key key;
    //用户数据库
    @Autowired
    protected UserRepository userRepository;
    //实际装入是 JwtUserDetailsService:
    private final UserDetailsService userDetailsService;

    @Autowired
    private RefreshTokenService refreshTokenService;

    @Value("${server.ssl.enabled:false}")
    private boolean  isSSLenabled;
    @Value("${sei.server.URI:}")
    private String serverURI;

    public JwtTokenProvider(
            @Value("${jwt.secret}") String secret,
            @Value("${jwt.access-token-validity-in-seconds}") long accessTokenValidityInSeconds,
            @Value("${jwt.refresh-token-validity-in-seconds}") long refreshTokenValidityInSeconds,
            UserDetailsService userDetailsService){
        this.secret = secret;
        this.accessTokenValidityInMilliseconds = accessTokenValidityInSeconds * 1000;
        this.refreshTokenValidityInMilliseconds = refreshTokenValidityInSeconds * 1000;
        this.userDetailsService = userDetailsService;
    }

    @Override
    public void afterPropertiesSet() {
        byte[] keyBytes = Decoders.BASE64.decode(secret);
        this.key = Keys.hmacShaKeyFor(keyBytes);
    }

    // 结果类
    public static class TokenValidationResult {
        private final boolean valid;
        private final String username;
        private final String errorMessage;

        public TokenValidationResult(boolean valid, String username, String errorMessage) {
            this.valid = valid;
            this.username = username;
            this.errorMessage = errorMessage;
        }

        // getters
        public boolean isValid() { return valid; }
        public String getUsername() { return username; }
        public String getErrorMessage() { return errorMessage; }
    }
    /** 需要在authenticate()接口进行单点登录适应机制改造的，而不是这里。
     * 假如SSO单点登录 auth0 authing登录云服务 OAuth2: 也是需要本地签发自己的Token已经本地角色授权，本地服务只是缺少密码验证和密码存储环节。
     * camunda-Identity模块：而且要配套 Keycloak 实例运行;  没云容器无法本地方式安装；	  https://docs.camunda.io/docs/self-managed/identity/getting-started/
     * spring-boot-keycloak-starter  集成的单点登录+授权。
     * OpenID Connect/OIDC使用OAuth2.0;  Keycloak用于REST;配合Spring MVC的@RequestMapping;
     * 认证（Authentication）即确认该用户的身份是他所声明的那个人; 授权（Authorization）即根据用户身份授予他访问特定资源的权限;
     * CAS是一种仅用于Authentication的服务，它和OAuth/OIDC协议不一样，并不能作为一种Authorization的协议。
     * OAuth 2.0协议，它是一种Authorization协议并不是一种Authentication协议;
     * SAML协议 不支持移动端APP非浏览器的SSO;  旧的+UI管理器不适合前后端分离架构的。
     * Keycloak基于OAuth 2.1、Open ID Connect、JSON Web Token（JWT）和SAML 2.0规范，为浏览器应用和RESTful Web Service提供SSO和IDM集成;
     * Keycloak前端采用了AngularJS技术; ID plvider
     * pac4j引擎;  cas 认证中心;  PKCE with OpenID Connect手机端java APP;
     * OpenID Connect; 原理: https://blog.csdn.net/qq_24550639/article/details/111089296
     * 部署 Keycloak 服务器,独立运行的; Keycloak OIDC Provider ;
     * JWT伪造   https://blog.csdn.net/qq_45557476/article/details/123171281
     * */
    public TokenDto createToken(Authentication authentication, DeviceInfo deviceInfo) {
        Date now = new Date();

        // 创建access token，包含设备ID
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .setIssuedAt(now)
                .claim(DEVICE_ID_KEY, deviceInfo.getDeviceId())
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(new Date(now.getTime() + accessTokenValidityInMilliseconds))
                .compact();
        //实际上redis: KV存储的ID充当做refreshToken： 通过RefreshTokenService创建refresh token并存储到Redis
        String refreshTokenId = refreshTokenService.createRefreshToken(
                authentication.getName(),
                deviceInfo, -1L
        );
        return TokenDto.builder()
                .accessToken(accessToken)
                .refreshToken(refreshTokenId)
                .accessTokenExpiresIn(accessTokenValidityInMilliseconds / 1000)
                .refreshTokenExpiresIn(refreshTokenValidityInMilliseconds / 1000)
                .build();
    }

    public TokenDto createAccessToken(Authentication authentication) {
        Date now = new Date();
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .setIssuedAt(now)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(new Date(now.getTime() +accessTokenValidityInMilliseconds))
                .compact();
        return TokenDto.builder()
                .accessToken(accessToken)
                .accessTokenExpiresIn(accessTokenValidityInMilliseconds / 1000)
                .build();
    }

    public TokenDto createAccessToken(Authentication authentication, String deviceId) {
        Date now = new Date();
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .setIssuedAt(now)
                .claim(DEVICE_ID_KEY, deviceId)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(new Date(now.getTime() + accessTokenValidityInMilliseconds))
                .compact();
        return TokenDto.builder()
                .accessToken(accessToken)
                .accessTokenExpiresIn(accessTokenValidityInMilliseconds / 1000)
                .build();
    }

    public TokenDto createUserAccessToken(String username) {
        Date now = new Date();
        String accessToken = Jwts.builder()
                .setSubject(username)
                .setIssuedAt(now)
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(new Date(now.getTime() +accessTokenValidityInMilliseconds))
                .compact();
        return TokenDto.builder().accessToken(accessToken).build();
    }

    public Authentication getAuthentication(String token) {
        Claims claims = Jwts
                .parser()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();

        Object auths= claims.get(AUTHORITIES_KEY);
        Collection<? extends GrantedAuthority> authorities =null;
        if(null!=auths) {
            authorities = Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        } else {
            String username= claims.getSubject();
            md.system.User  user=userRepository.findByUsername(username);
            if(null!=user && null!=user.heHasRoles()){
                authorities =user.heHasRoles().stream()
                        .map(authority -> new SimpleGrantedAuthority(authority.getName().name()))
                        .collect(Collectors.toList());
            }
        }
        //权限认证应该以内存中最后的authentication{..}的为准。数据库读取是恢复步骤。
        User principal = new User(claims.getSubject(), "", authorities);

        return new UsernamePasswordAuthenticationToken(principal, token, authorities);
    }

    public TokenValidationResult validateToken(String token) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
            String username = claims.getSubject(); // 标准做法是使用subject
            return new TokenValidationResult(true, username, null);
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("无效token: {}", e.getMessage());
            return new TokenValidationResult(false, null, "无效token");
        } catch (ExpiredJwtException e) {
            log.info("token已过期: {}", e.getMessage());
            return new TokenValidationResult(false, null, "token已过期");
        } catch (IllegalArgumentException | UnsupportedJwtException e) {
            log.info("token格式错误: {}", e.getMessage());
            return new TokenValidationResult(false, null, "token格式错误");
        }
    }

    public Boolean validateRefreshToken(String refreshToken){
        try{
            Jws<Claims> claims = Jwts.parser().setSigningKey(key).build().parseClaimsJws(refreshToken);
            if(!claims.getBody().getExpiration().before(new Date())){
                log.info("正常的");
            }
        }
        catch (Exception e){
            log.info("超期");
            return Boolean.FALSE;
        }
        return Boolean.TRUE;
    }

    public TokenDto reCreateToken(Authentication authentication) {

        long now = (new Date()).getTime();
        Date validity = new Date(now + this.accessTokenValidityInMilliseconds);

        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .signWith(key, SignatureAlgorithm.HS512)
                .setExpiration(validity)
                .compact();

        log.info(accessToken);

        return TokenDto.builder().accessToken(accessToken).build();
    }
    //根据token为其续命,更新对应的spring security; 每一次的请求包都要首先恢复身份证明的。
    protected boolean continuedTokenLifeAuthentication(String token, HttpServletRequest request, HttpServletResponse response) throws IOException {
        if(!StringUtils.hasText(token))
            return true;     //没有token证书，允许过，但受到具体接口的内部安全措施getContext().setAuthentication(authentication)控制。
        String usernameIdc = null;
        Claims claims =null;
        try {
            claims =Jwts.parser().setSigningKey(key).build().parseClaimsJws(token).getBody();
        } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
            log.info("无效token");
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            return false;
        } catch (ExpiredJwtException | IllegalArgumentException | UnsupportedJwtException e) {
            log.info("过期token");
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Token has expired. Please log in again.");
            return false;
        }
        //绕了个大弯 (Claims::getSubject).apply(claims);
        try {
            //username = (Claims::getSubject).apply(claims);   为何手机电脑同时登录，token时间容易失效。
            usernameIdc = claims.getSubject();
        } catch (IllegalArgumentException | ExpiredJwtException e) {
            log.warn("nValid Token", e);
        }
        if(null==usernameIdc)	    return true;

        //用JwtUserDetailsService从数据库找的，　UserDetails这里就是JwtUser了;
        UserDetails userDetails = userDetailsService.loadUserByUsername(usernameIdc);
        //登录已经过期了，应当抛出异常，否则返回都是null，不知道到底是何种情况内还是应用层没有数据。
        if(!validateClaims(claims, userDetails))      return false;
        //用户已经被屏蔽了。
        if(!userDetails.isEnabled())       return false;
        //因为Session .STATELESS每一次请求包都需要再次设置身份信息，实际上这里每次请求包都要过。
        //credentials 证明书? 公钥的Key吗;
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        // logger.info("authorizated user '{}', Reset security context", username);
        //这里应该有ROLE_:实际就是userId保留到Context
        SecurityContextHolder.getContext().setAuthentication(authentication);
        return true;
    }

    //验证合法性的token; 而每一次登录都会重新生成token的。
    private Boolean validateClaims(Claims claims, UserDetails userDetails) {
        JwtUser user = (JwtUser) userDetails;
        String username = claims.getSubject();      //绕了个大弯 (Claims::getSubject).apply(claims);
        Date created = claims.getIssuedAt();   //"iat": getIssuedAtDateFromToken(token);
        //final Date expiration = getExpirationDateFromToken(token);
        boolean isTokenExpired;
        final Date expiration = claims.getExpiration();
        Date now = new Date();
        isTokenExpired = expiration.before(now);         //在now以前的
        //密码修改了，lastPasswordReset；
        //username已改成userID了 代替做标识。
        return (
                username.equals( user.getUsername() )
                        && !isTokenExpired
                        && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
        );
    }
    //密码修改 ？ 没有配置上发行时间没法做到!。 claims.getExpiration(); PWA离线token没有设置iat时间的就忽略。
    private Boolean isCreatedBeforeLastPasswordReset(Date created, Date lastPasswordReset) {
        if(null==created)    return false;
        return (lastPasswordReset != null && created.before(lastPasswordReset));
    }

    public String getServerURI() {
        return serverURI;
    }

    public String getDeviceIdFromToken(String token) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token)
                    .getBody();
            return claims.get(DEVICE_ID_KEY, String.class);
        } catch (Exception e) {
            log.warn("无法从token提取设备ID: {}", e.getMessage());
            return null;
        }
    }
}
